﻿#region Includes
using System;
using System.IO;
using System.Text;
using System.Drawing;
using System.Diagnostics;
using System.Collections.Generic;

using OpenTK;
using OpenTK.Graphics.OpenGL;

using GingerGL.IO;
using GingerGL.Math;
using GingerGL.Drawing;
using GingerGL.Drawing.MD3;
using GingerGL.Graphics;
#endregion

namespace GingerGL.Drawing
{
    //------------------------------------------------------------//
    /// <summary>
    /// Quake Model Instance
    /// </summary>
    public class QuakeModel : Entity, IDrawable
    {
        //--------------------------------------------------------//
        #region Delegates & Events
        public delegate void SequenceEvent(QuakeModel sender, string name, ComponentType part);
        public delegate void SequenceFrameEvent(QuakeModel sender, int frame, ComponentType part);
        /// <summary>
        /// Notification that the Animation Looped
        /// </summary>
        public event SequenceEvent AnimationLooped;
        /// <summary>
        /// Notification that the Animation Ended
        /// </summary>
        public event SequenceEvent AnimationEnded;
        /// <summary>
        /// Notification that the Frame Changed
        /// </summary>
        public event SequenceFrameEvent FrameChanged;
        #endregion
        //--------------------------------------------------------//
        #region Variables
        protected Matrix4 LegTorsoMatrix = Matrix4.Identity;
        protected Matrix4 TorsoHeadMatrix = Matrix4.Identity;
        #endregion
        //--------------------------------------------------------//
        #region Properties
        /// <summary>
        /// Gets/Sets the Frame Per Second Scaling
        /// </summary>
        static public float FPSScale { get; set; }
        /// <summary>
        /// Gets/Sets TRUE to draw tag markers
        /// </summary>
        static public bool DrawTags { get; set; }
        /// <summary>
        /// This is internal animation tracking
        /// </summary>
        internal QuakeAnimationTracker LegSequence { get; set; }
        /// <summary>
        /// This is internal animation tracking
        /// </summary>
        internal QuakeAnimationTracker TorsoSequence { get; set; }
        /// <summary>
        /// Gets MD3 Import Data
        /// </summary>
        public Model Character { get; protected set; }
        /// <summary>
        /// Gets Tags
        /// </summary>
        public Dictionary<Tag, Matrix4> TagMatrixOffsets { get; protected set; }
        /// <summary>
        /// Gets a list of hidden meshs
        /// </summary>
        public List<AnimatedMesh> HiddenMeshes { get; protected set; }
        /// <summary>
        /// Gets a list of textures based on mesh
        /// </summary>
        public Dictionary<AnimatedMesh, Texture> SkinTextures { get; protected set; }
        /// <summary>
        /// Gets the current skin name
        /// </summary>
        public string SkinName { get; protected set; }
        #endregion
        //--------------------------------------------------------//
        #region Constructors
        /// <summary>
        /// Initializes a new instance of object
        /// </summary>
        /// <param name="c">Loaded MD3 Model</param>
        public QuakeModel(Model c)
        {
            this.Character = c;
            this.TagMatrixOffsets = new Dictionary<Tag, Matrix4>();
            this.HiddenMeshes = new List<AnimatedMesh>();
            this.SkinName = String.Empty;
        }
        /// <summary>
        /// Initializes a new instance of object
        /// </summary>
        static QuakeModel()
        {
            FPSScale = 1.0f;
            DrawTags = false;
        }
        #endregion
        //--------------------------------------------------------//
        #region Methods
        /// <summary>
        /// Bind Texture to OpenGL
        /// </summary>
        /// <param name="path">MD3 Path to File</param>
        /// <returns>OpenGL Texture ID</returns>
        private Texture BindTexture(string path)
        {
            if (!Character.FileSystem.Exists(path))
                if (Character.FileSystem.Exists(path.Split('.')[0] + ".jpg"))
                    path = path.Split('.')[0] + ".jpg";
                else
                    throw new FileNotFoundException(path);

            if (System.IO.Path.GetExtension(path) == ".tga")
                return Texture.Instance.FromImage(
                    TGAFile.Load(
                        Character.FileSystem.Open(path)));
            else
                return Texture.Instance.FromImage(
                    Image.FromStream(
                        Character.FileSystem.Open(path)));
        }
        /// <summary>
        /// Binds Skin to Model
        /// </summary>
        public void BindSkin()
        {
            this.SkinTextures = new Dictionary<AnimatedMesh, Texture>();
            Skin skin;
            if (Character.SkinExists(this.SkinName))        skin = Character.GetSkin(this.SkinName);
            else if (Character.SkinExists("default"))       skin = Character.GetSkin("default");
            else if (Character.SkinExists(string.Empty))    skin = Character.GetSkin(string.Empty);
            else                                            skin = Character.SkinFromSurfs();
            
            foreach (KeyValuePair<string, Dictionary<string, string>> SkinComponent in skin.Surfaces)
            {
                Component comp = Character.FindComponent(SkinComponent.Key);
                if (comp != null)
                {
                    foreach (KeyValuePair<string, string> meshTextures in SkinComponent.Value)
                    {
                        AnimatedMesh mesh = comp.FindMesh(meshTextures.Key);
                        if (mesh != null)
                            this.SkinTextures.Add(mesh, BindTexture(meshTextures.Value));
                    }
                }
            }
        }
        /// <summary>
        /// Draws QuakeModel
        /// </summary>
        /// <returns>TRUE if successful</returns>
        public override void Draw(FrameEventArgs e)
        {
            UpdateSequence();                                   //update animation

            if (Character.RootNode == null)                     //dblchk is this a root?
                return;                                         //zomg!
            if (this.SkinTextures == null)                      //did we create the textures?
                BindSkin();                                     //bind them
            GL.PushMatrix();                                    //save state
            GL.Rotate(90, -1, 0, 0);
            DrawComponent(Character.RootNode);                  //draw parts
            GL.PopMatrix();                                     //restore matrix
        }
        /// <summary>
        /// Update Animations
        /// </summary>
        private void UpdateSequence()
        {
            if (LegSequence != null) LegSequence.Update();
            if (TorsoSequence != null) TorsoSequence.Update();
        }
        /// <summary>
        /// Get Current Frame
        /// </summary>
        internal int GetFrame(QuakeAnimationTracker tracker)
        {
            if (tracker == null) return 0;
            return tracker.ThisFrame;
        }
        /// <summary>
        /// Get Animation Tracker
        /// </summary>
        private QuakeAnimationTracker GetTracker(ComponentType part)
        {
            if (part == ComponentType.Head || part == ComponentType.Other) return null;
            if (part == ComponentType.Legs) return LegSequence;
            else return TorsoSequence;
        }
        /// <summary>
        /// Draws Component
        /// </summary>
        private void DrawComponent(ConnectedComponent component)
        {
            int thisFrame = GetFrame(GetTracker(component.Part.PartType));
            foreach (AnimatedMesh mesh in component.Part.Meshes)
                DrawMesh(mesh, thisFrame, component.Part.PartType);

            foreach (KeyValuePair<Tag, List<ConnectedComponent>> child in component.Children)
            {
                if (child.Key.Frames.Length > 0)
                {
                    GL.PushMatrix();

                    int realFrame = 0;
                    if (thisFrame < child.Key.Frames.Length)
                        realFrame = thisFrame;

                    FrameMatrix mat = child.Key.Frames[realFrame];
                    GL.MultMatrix(ref mat.Matrix);

                    if (DrawTags)
                    {
                        float tagSize = 2.0f;
                        GL.Disable(EnableCap.Texture2D);
                        GL.Disable(EnableCap.Lighting);

                        GL.Begin(BeginMode.Lines);
                        GL.Color4(Color.Red);
                        GL.Vertex3(0, 0, 0);
                        GL.Vertex3(tagSize, 0, 0);
                        GL.Color4(Color.Green);
                        GL.Vertex3(0, 0, 0);
                        GL.Vertex3(0, tagSize, 0);
                        GL.Color4(Color.Blue);
                        GL.Vertex3(0, 0, 0);
                        GL.Vertex3(0, 0, tagSize);
                        GL.End();
                        GL.Color4(Color.White);

                        GL.Enable(EnableCap.Lighting);
                    }

                    foreach (ConnectedComponent c in child.Value)
                    {
                        Tag destTag = c.Part.FindTag(child.Key.Name);
                        if (destTag ==null) continue;

                        GL.PushMatrix();
                        if (component.Part.PartType == ComponentType.Legs && c.Part.PartType == ComponentType.Torso)
                            GL.MultMatrix(ref LegTorsoMatrix);
                        else if (component.Part.PartType == ComponentType.Torso && c.Part.PartType == ComponentType.Head)
                            GL.MultMatrix(ref TorsoHeadMatrix);

                        Matrix4 m = destTag.Frames[0].Matrix;
                        if (thisFrame < destTag.Frames.Length) m = destTag.Frames[thisFrame].Matrix;
                        GL.MultMatrix(ref m);
                        DrawComponent(c);
                        GL.PopMatrix();
                    }
                    GL.PopMatrix();
                }
            }
        }
        /// <summary>
        /// Draws Mesh
        /// </summary>
        /// <param name="mesh">Mesh to Draw</param>
        /// <param name="thisFrame">Frame</param>
        private void DrawMesh(AnimatedMesh mesh, int thisFrame, ComponentType type)
        {
            if (HiddenMeshes.Contains(mesh) || mesh.Frames.Count < 1)                  //does the frame exist?
                return;

            GL.Color4(Color.White);                                                    //for good measure
            Frame frame = (thisFrame >= mesh.Frames.Count) ?                           //to loop or not to loop, that is the question!
                mesh.Frames[0] : mesh.Frames[thisFrame];

            if (!frame.Vertices.IsCommited) 
                frame.Vertices.Commit();                                                //load buffers
            GL.PushAttrib(AttribMask.EnableBit);                                        //save enables
            GL.PushClientAttrib(ClientAttribMask.ClientVertexArrayBit);                 //save current array state
            //----------------------------------------------------//
            #region Vertex Buffer
            GL.BindBuffer(BufferTarget.ArrayBuffer, frame.Vertices.VertexBufferId);     //bind to the array buffer id
            GL.VertexPointer                                                            //set the Pointer to the current bound array describing how the data ia stored
            (
                3,
                VertexPointerType.Float,
                Vertex.SizeInBytes,
                Vertex.Offset(VertexOffset.Position)
            );
            GL.EnableClientState(ArrayCap.VertexArray);                                 //enable the client state so it will use this array buffer pointer
            #endregion
            //----------------------------------------------------//
            #region Normals
            GL.NormalPointer(                                                           //set the Pointer to the current bound array describing how the data ia stored
                NormalPointerType.Float,
                Vertex.SizeInBytes,
                Vertex.Offset(VertexOffset.Normal));
            GL.EnableClientState(ArrayCap.NormalArray);                                 // Enable the client state so it will use this array buffer pointer
            #endregion
            //----------------------------------------------------//
            #region Textures
            //----------------------------------------------------//
            GL.ClientActiveTexture(TextureUnit.Texture0);                               //set the active state to texture0
            GL.TexCoordPointer(                                                         //create point for this texture
                2,
                TexCoordPointerType.Float,
                Vertex.SizeInBytes,
                Vertex.Offset(VertexOffset.Texture0));
            GL.EnableClientState(ArrayCap.TextureCoordArray);                           //enable tex coords
            this.SkinTextures[mesh].Apply();                                            //apply texture
            #endregion
            //----------------------------------------------------//
            #region Elements
            // Element Array Buffer
            GL.BindBuffer(                                                              // Bind to the Array Buffer ID
                BufferTarget.ElementArrayBuffer, 
                mesh.Triangles.IndexBufferId);
            #endregion
            //----------------------------------------------------//
            #region Drawing Elements
            // Draw the elements in the element array buffer
            // Draws up items in the Color, Vertex, TexCoordinate, and Normal Buffers using indices in the ElementArrayBuffer
            GL.DrawElements                                                             //draw buffers
            (
                BeginMode.Triangles,
                mesh.Triangles.Count,
                DrawElementsType.UnsignedInt,
                IntPtr.Zero
            );
            #endregion
            //----------------------------------------------------//
            GL.PopClientAttrib();                                                       // Restore the state
            GL.PopAttrib();  
        }
        /// <summary>
        /// Hide Mesh
        /// </summary>
        public void HideMesh(string name)
        {
            foreach (Component c in Character.Components)
            {
                foreach (AnimatedMesh m in c.Meshes)
                {
                    if (m.Name == name)
                    {
                        if (!HiddenMeshes.Contains(m))
                            HiddenMeshes.Add(m);
                    }
                }
            }
        }
        /// <summary>
        /// Show Mesh
        /// </summary>
        public void ShowMesh(string name)
        {
            foreach (Component c in Character.Components)
                foreach (AnimatedMesh m in c.Meshes)
                    if (m.Name == name)
                        if (HiddenMeshes.Contains(m)) HiddenMeshes.Remove(m);
        }
        /// <summary>
        /// Set Skin
        /// </summary>
        public void SetSkin(string name)
        {
            this.SkinName = name;
            BindSkin();
        }
        /// <summary>
        /// Set Torso Animation
        /// </summary>
        public bool SetTorsoSequence(string name)
        {
            AnimationSequence sequence = Character.FindSequence("torso", name);
            if (sequence.IsNull()) { TorsoSequence = null; return false; }

            TorsoSequence = new QuakeAnimationTracker(sequence);
            TorsoSequence.AnimationEnded += new QuakeAnimationTracker.SequenceEvent(torsoSequence_AnimationEnded);
            TorsoSequence.AnimationLooped += new QuakeAnimationTracker.SequenceEvent(torsoSequence_AnimationLooped);
            if (FrameChanged != null)
                TorsoSequence.FrameChanged += new QuakeAnimationTracker.SequenceEvent(torsoSequence_FrameChanged);
            return true;
        }
        /// <summary>
        /// Torso Sequence Animation Frame Changed Overload
        /// </summary>
        private void torsoSequence_FrameChanged(QuakeAnimationTracker sender, string name)
        { if (FrameChanged != null) FrameChanged(this, sender.ThisFrame, ComponentType.Torso); }
        /// <summary>
        /// Torso Sequence Animation Looped Overload
        /// </summary>
        private void torsoSequence_AnimationLooped(QuakeAnimationTracker sender, string name)
        { if (AnimationLooped != null) AnimationLooped(this, name, ComponentType.Torso); }
        /// <summary>
        /// Torso Sequence Animation Ended Overload
        /// </summary>
        private void torsoSequence_AnimationEnded(QuakeAnimationTracker sender, string name)
        { if (AnimationEnded != null) AnimationEnded(this, name, ComponentType.Torso); }
        /// <summary>
        /// Sets the Leg Sequence
        /// </summary>
        /// <param name="name">Animation Name</param>
        public bool SetLegSequence(string name)
        {
            AnimationSequence sequence = Character.FindSequence("legs", name);
            if (sequence.IsNull()) { LegSequence = null; return false; }

            LegSequence = new QuakeAnimationTracker(sequence);
            LegSequence.AnimationEnded += new QuakeAnimationTracker.SequenceEvent(legSequence_AnimationEnded);
            LegSequence.AnimationLooped += new QuakeAnimationTracker.SequenceEvent(legSequence_AnimationLooped);
            if (FrameChanged != null)
                LegSequence.FrameChanged += new QuakeAnimationTracker.SequenceEvent(legSequence_FrameChanged);
            return true;
        }
        /// <summary>
        /// Leg Sequence Animation Frame Changes Overload
        /// </summary>
        private void legSequence_FrameChanged(QuakeAnimationTracker sender, string name)
        { if (FrameChanged != null) FrameChanged(this, sender.ThisFrame, ComponentType.Legs); }
        /// <summary>
        /// Leg Sequence Animation Looped Overload
        /// </summary>
        private void legSequence_AnimationLooped(QuakeAnimationTracker sender, string name)
        { if (AnimationLooped != null) AnimationLooped(this, name, ComponentType.Legs); }
        /// <summary>
        /// Leg Sequence Animation Ended Overload
        /// </summary>
        private void legSequence_AnimationEnded(QuakeAnimationTracker sender, string name)
        { if (AnimationEnded != null) AnimationEnded(this, name, ComponentType.Legs); }
        #endregion
        //--------------------------------------------------------//
    }
    //------------------------------------------------------------//
}
